﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Drawing2D;
using WbsEditor.Extensions.Disposables;
using WbsEditor.Extensions.Matrices;
using WbsEditor.Extensions.Points;
using System.ComponentModel;
using System.Drawing.Text;
using System.Diagnostics;

namespace WbsEditor
{
    /// <summary>
    ///     Provides scrolling and zooming for user drawn content.
    /// </summary>
    public /*abstract*/ class ScrollableZoomableControl
        : UserControl
    {
        #region Constants

        private const float MinimumZoomFactor = 0.1f;
        private const float MaximumZoomFactor = 5.0f;

        #endregion

        #region Constructor and Initialization

        public ScrollableZoomableControl()
        {
            this.BackColor = Color.White;
            this.AutoScroll = false;
            this._zoom = 1.0f;

            this._timer = new Timer();
            this._timer.Interval = 250;
            this._timer.Tick += (o, e) =>
            {
                this._timer.Stop();
                this._lowQualityRender = false;

                this.Invalidate();
            };

            this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.OptimizedDoubleBuffer | ControlStyles.ResizeRedraw | ControlStyles.UserPaint, true);
        }

        #endregion

        #region Properties and Fields

        private bool _layoutDirty;
        private float _zoom;
        private bool _center;
        private Size _unscaledContentSize;
        private bool _lowQualityRender;
        private Timer _timer;
        private Point _lastAutoScrollPosition;

        private Matrix _scaleTransform;
        private Matrix _renderTransform;
        private Matrix _invertedScaleTransform;
        private Matrix _invertedRenderTransform;

        /// <summary>
        ///     The unscaled size of the content to draw.
        /// </summary>
        /// <remarks>
        ///     This value is calculated by the <see cref="Measure"/> method.
        /// </remarks>
        protected Size ContentSize
        {
            get
            {
                if (this._layoutDirty)
                {

                    this._unscaledContentSize = this.Measure(this.VirtualSize);
                    this._layoutDirty = false;
                }

                return this._unscaledContentSize;
            }
        }
        protected int ContentHeight
        {
            get
            {
                return this.ContentSize.Height;
            }
        }
        protected int ContentWidth
        {
            get
            {
                return this.ContentSize.Width;
            }
        }
        /// <summary>
        ///     Gets a valud indicating whether the content should be centered.
        /// </summary>
        protected virtual bool Center
        {
            get
            {
                return this._center;
            }
            set
            {
                if (this._center == value)
                    return;

                this._center = value;
                this.Remeasure();
            }
        }
        /// <summary>
        ///     The size this control has in units of the content.
        /// </summary>
        /// <remarks>
        ///     This property is useful if the content should fill the control regardless of the zoom level.
        /// </remarks>
        protected Size VirtualSize
        {
            get
            {
                Size available = this.ClientSize;

                available.Width -= this.Padding.Horizontal;
                available.Height -= this.Padding.Vertical;
                available = this.InvertedScaleTransform.TransformSize(available);

                return available;
            }
        }

        private Size ContentSizeWithPadding
        {
            get
            {
                return new Size(this.ContentSize.Width + this.Padding.Horizontal, this.ContentSize.Height + this.Padding.Vertical);
            }
        }
        private Size NeededSize
        {
            get
            {
                Size result = this.ContentSizeWithPadding;

                result.Width = (int)(result.Width * this._zoom + 1);
                result.Height = (int)(result.Height * this._zoom + 1);

                return result;
            }
        }
        private Point Offset
        {
            get
            {
                Point result = new Point();

                result = result.Add(this.AutoScrollPosition);
                result = result.Add(this.Padding.Left, this.Padding.Top);

                if (this.Center)
                {
                    if (this.NeededSize.Width < this.ClientSize.Width)
                        result.X += (int)((this.ClientSize.Width - this.NeededSize.Width) / 2.0f + 0.5f);
                    if (this.NeededSize.Height < this.ClientSize.Height)
                        result.Y += (int)((this.ClientSize.Height - this.NeededSize.Height) / 2.0f + 0.5f);
                }

                return result;
            }
        }

        private Matrix ScaleTransform
        {
            get
            {
                if (this._scaleTransform == null)
                {
                    this._scaleTransform = new Matrix();
                    this._scaleTransform.Scale(this._zoom, this._zoom);
                }

                return this._scaleTransform;
            }
        }
        private Matrix RenderTransform
        {
            get
            {
                if (this._renderTransform == null)
                {
                    this._renderTransform = new Matrix();
                    this._renderTransform.Translate(this.Offset.X, this.Offset.Y);
                    this._renderTransform.Scale(this._zoom, this._zoom);
                }

                return this._renderTransform;
            }
        }
        private Matrix InvertedScaleTransform
        {
            get
            {
                if (this._invertedScaleTransform == null)
                {
                    this._invertedScaleTransform = this.ScaleTransform.CreateInvertedClone();
                }

                return this._invertedScaleTransform;
            }
        }
        private Matrix InvertedRenderTransform
        {
            get
            {
                if (this._invertedRenderTransform == null)
                {
                    this._invertedRenderTransform = this.RenderTransform.CreateInvertedClone();
                }

                return this._invertedRenderTransform;
            }
        }

        /// <summary>
        ///     The zoom level.
        /// </summary>
        /// <remarks>
        ///     1.0 stand for 100% zoom or the original size.
        /// </remarks>
        /// <seealso cref="ZoomChanged"/>
        [Category("Appearance")]
        [Browsable(true)]
        [DesignerSerializationVisibility(DesignerSerializationVisibility.Visible)]
        [DefaultValue(1.0f)]
        public float Zoom
        {
            get
            {
                return this._zoom;
            }
            set
            {
                if (this._zoom == value)
                    return;

                value = Math.Max(MinimumZoomFactor, value);
                value = Math.Min(MaximumZoomFactor, value);

                this._zoom = value;
                this.OnZoomChanged();
            }
        }

#endregion

        #region Events

        public event EventHandler ZoomChanged;

        private void OnZoomChanged()
        {
            this.EnableAutoScrollIfNecessary();
            this.InvalidateMatrices();
            this.Invalidate();

            EventHandler handler = this.ZoomChanged;
            if (handler != null)
                handler(this, EventArgs.Empty);
        }

        private void OnAutoScrollPositionChanged()
        {
            this._lastAutoScrollPosition = this.AutoScrollPosition;

            this.BeginMoveScrollZoom();
            this.InvalidateMatrices();
        }

        #endregion

        #region Overriden Event Handlers

        protected sealed override void OnPaintBackground(PaintEventArgs e)
        {
            e.Graphics.Clear(this.BackColor);
        }

        protected sealed override void OnPaint(PaintEventArgs e)
        {
            if (this._lastAutoScrollPosition != this.AutoScrollPosition)
                this.OnAutoScrollPositionChanged();

            Graphics g = e.Graphics;
            if (this._lowQualityRender)
            {
                g.CompositingQuality = CompositingQuality.HighSpeed;
                g.SmoothingMode = SmoothingMode.HighSpeed;
                g.TextRenderingHint = TextRenderingHint.SingleBitPerPixel;
            }
            else
            {
                g.CompositingQuality = CompositingQuality.HighQuality;
                g.SmoothingMode = SmoothingMode.AntiAlias;
                g.TextRenderingHint = TextRenderingHint.AntiAliasGridFit;
            }

            g.Transform = this.RenderTransform;

            this.OnPaintContent(e.Graphics);

            g.ResetTransform();
        }

        protected override void OnPaddingChanged(EventArgs e)
        {
            this.InvalidateMatrices();
            this.Invalidate();

            base.OnPaddingChanged(e);
        }

        protected override void OnClientSizeChanged(EventArgs e)
        {
            this.InvalidateMatrices();
            this.Remeasure();
            this.EnableAutoScrollIfNecessary();

            base.OnClientSizeChanged(e);
        }

        protected override void OnMouseWheel(MouseEventArgs e)
        {
            Debug.Assert(e.Delta != 0);

            if ((ModifierKeys & Keys.Control) == Keys.Control)
            {
                this.BeginMoveScrollZoom();

                Point oldModelPoint = this.InvertedRenderTransform.TransformPoint(e.Location);
                this.Zoom = this.Zoom + e.Delta / 1200.0f;
                Point newModelPoint = this.InvertedRenderTransform.TransformPoint(e.Location);

                Point modelDifference = new Point(newModelPoint.X - oldModelPoint.X, newModelPoint.Y - oldModelPoint.Y);
                Point offset = Point.Round(modelDifference.Multiply(this.Zoom));

                this.AutoScrollPosition = this.AutoScrollPosition.Multiply(-1).Add(offset.Multiply(-1));
                this.OnAutoScrollPositionChanged();
            }
            else
                base.OnMouseWheel(e);
        }

        #endregion

        #region Transformations

        /// <summary>
        ///     Transforms a point in client coordinates to content coordinates.
        /// </summary>
        /// <param name="p">
        ///     A point in client coordinates (scaled and translated).
        /// </param>
        /// <returns>
        ///     A point in content coordinates (unscaled and untranslated).
        /// </returns>
        protected Point ClientPointToContent(Point p)
        {
            return this.InvertedRenderTransform.TransformPoint(p);
        }
        /// <summary>
        ///     Transforms a point in content coordinates to client coordinates.
        /// </summary>
        /// <param name="p">
        ///     A point in content coordinates (unscaled and untranslated).
        /// </param>
        /// <returns>
        ///     A point in client coordinates (scaled and translated).
        /// </returns>
        protected Point VirtualToClient(Point p)
        {
            return this.RenderTransform.TransformPoint(p);
        }

        #endregion

        #region Viewport Manipulation

        /// <summary>
        ///     Ensures that a specified area of the content is fully visible to the user.
        /// </summary>
        /// <param name="area">
        ///     The area which should be fully visible. The units are content related unites, i.e. unscaled and untranslated.
        /// </param>
        protected void EnsureVisible(Rectangle area)
        {
            Point screenLocation = this.RenderTransform.TransformPoint(area.Location);
            Size screenSize = this.ScaleTransform.TransformSize(area.Size);
            Rectangle nodeScreenBounds = new Rectangle(screenLocation, screenSize);

            Rectangle visibleBounds = this.ClientRectangle;
            Point autoScrollPosition = this.AutoScrollPosition.Multiply(-1);

            if (nodeScreenBounds.Left < visibleBounds.Left)
                autoScrollPosition.X -= visibleBounds.Left - nodeScreenBounds.Left;
            if (nodeScreenBounds.Right > visibleBounds.Right)
                autoScrollPosition.X += nodeScreenBounds.Right - visibleBounds.Right;
            if (visibleBounds.Top > nodeScreenBounds.Top)
                autoScrollPosition.Y -= visibleBounds.Top - nodeScreenBounds.Top;
            if (visibleBounds.Bottom < nodeScreenBounds.Bottom)
                autoScrollPosition.Y += nodeScreenBounds.Bottom - visibleBounds.Bottom;

            if (this.AutoScrollPosition != autoScrollPosition.Multiply(-1))
            {
                this.AutoScrollPosition = autoScrollPosition;
                this.OnAutoScrollPositionChanged();

                this.Invalidate();
            }
        }
        /// <summary>
        ///     Causes a remasure of the content size.
        /// </summary>
        /// <seealso cref="Measure"/>
        protected void Remeasure()
        {
            this._layoutDirty = true;

            this.EnableAutoScrollIfNecessary();
            this.InvalidateMatrices();
            this.Invalidate();
        }

        private void EnableAutoScrollIfNecessary()
        {
            Size neededSize = this.NeededSize;
            Size availableSize = this.ClientSize;

            if (availableSize.Width < neededSize.Width
                || availableSize.Height < neededSize.Height)
            {
                this.AutoScroll = true;
                this.AutoScrollMinSize = neededSize;
            }
            else
                this.AutoScroll = false;
        }

        private void BeginMoveScrollZoom()
        {
            this._lowQualityRender = true;

            this._timer.Stop();
            this._timer.Start();
        }

        private void InvalidateMatrices()
        {
            this._renderTransform.DisposeIfNotNull();
            this._scaleTransform.DisposeIfNotNull();
            this._invertedRenderTransform.DisposeIfNotNull();
            this._invertedScaleTransform.DisposeIfNotNull();

            this._renderTransform = null;
            this._scaleTransform = null;
            this._invertedRenderTransform = null;
            this._invertedScaleTransform = null;
        }

        #endregion

        #region Methods intented to be overriden

        /// <summary>
        ///     Calculates the size of the content.
        /// </summary>
        /// <param name="available">
        ///     The available size in content coordinates.
        /// </param>
        /// <remarks>
        ///     If this method returns a value larger than <see cref="available"/>, scrollbars will be displayed.
        /// </remarks>
        /// <returns>
        ///     The size the content needs.
        /// </returns>
        /// <seealso cref="Remeasure"/>
        protected virtual Size Measure(Size available)
        {
            return new Size(0, 0);
        }
        /// <summary>
        ///     Allows painting the content without having to bother with zoom level or scroll position.
        /// </summary>
        /// <remarks>
        ///     The size available for painting is at least <see cref="ContentSize"/> and <see cref="VirtualSize"/>. If <see cref="ContentSize"/> is smaller than <see cref="VirtualSize"/>, scrollbars are displayed.
        /// </remarks>
        /// <param name="g">
        ///     The <see cref="Graphics"/> used to paint the content.
        /// </param>
        protected virtual void OnPaintContent(Graphics g)
        {
        }

        #endregion
    }
}
